package emulator.shell.GUI.debugger;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Observable;
import java.util.Observer;

import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.UIManager;

import emulator.assembler.Assembler;
import emulator.assembler.DataDescription;
import emulator.assembler.Instruction;
import emulator.hardware.HwNumber;
import emulator.hardware.debug.CpuProfiler;
import emulator.hardware.nmos6502.Cpu6502;
import emulator.util.AddressRange;

@SuppressWarnings("serial")
public class CodePanel extends ScrollView {
	private static final int ADDRESS_SPACE = 65536;
	private static final int LABEL_MAX = 16;
	private static final int X_LABEL = 15;
	private static final int X_OPERAND = 22 + LABEL_MAX;
	private static final int STATE_DISPLAY_CODE = 0;
	private static final int STATE_INIT_CPU_RUNNING = 1;
	private static final int STATE_INIT_ANALYZE_CODE = 2;
	private int panel_state;
	private CodeInfo code_info = null;
	private Cpu6502 cpu;
	private CodePanelPosition position = new CodePanelPosition(10, 0, 10);
	private boolean is_running;
	private CodePanelAnalyzer analyzer = new CodePanelAnalyzer();
	private boolean is_single_step;
	private JPopupMenu popup;
	private DebugPanel debug_panel;
	private CpuProfiler profiler = new CpuProfiler(ADDRESS_SPACE);
	private boolean profiler_active = false;

	public CodePanel(Assembler assembler, Cpu6502 cpu, byte[] memory, DebugPanel debug_panel)
	{
		super();
		
		code_info = new CodeInfo(memory, assembler, ADDRESS_SPACE);
		this.cpu = cpu;
		this.debug_panel = debug_panel;
		
		panel_state = STATE_INIT_CPU_RUNNING;
		is_running = true;
		is_single_step = false;
		
		setCurrentPosition(0);
		setCurrentRange(code_info.size());
		
		initPanel();
		
		updateRunState(cpu.isRunning());
		
		addKeyListener(new KeyboardListener());
	}

	private void initPanel() {
		setLayout(new BorderLayout());
		
		initScrollBar();

        add(new LineMarker(), BorderLayout.WEST);
        
        scroll_pane = new CodePane();
        add(scroll_pane, BorderLayout.CENTER);
        scroll_pane.addKeyListener(new KeyboardListener());
        addMouseWheelListener(new MouseScrollListener());
        
	    popup = new JPopupMenu();
	    addPopupItem("Run to cursor", "runto");
	    popup.addSeparator();
	    addPopupItem("Scroll to PC", "topc");
	    popup.addSeparator();
	    addPopupItem("Set breakpoint", "setbreak");
	    addPopupItem("Clear breakpoint", "delbreak");
	    popup.addSeparator();
	    addPopupItem("Unassemble", "analyze");
	}

	private JMenuItem addPopupItem(String caption, String command) {
		JMenuItem item = new JMenuItem(caption);
		item.setActionCommand(command);
	    item.addActionListener(new PopupMenuListener());
	    popup.add(item);
	    return item;
	}

	public void updateRunState(boolean run_state) {
		boolean update_panel = false;
		if (run_state)
		{
			is_running = true;
		}
		else if (!analyzer.isRunning())
		{
			if (panel_state == STATE_INIT_CPU_RUNNING)
			{
				panel_state = STATE_INIT_ANALYZE_CODE;
				update_panel = true;
			}
			is_running = false;
			int address = cpu.getPC();
			if (is_single_step)
			{
				is_single_step = false;
				code_info.analyzeOneInstruction(address);
				setCurrentRange(code_info.size());
				ensureVisible(code_info.getIndexForAddress(address), false);
				panel_state = STATE_DISPLAY_CODE;
				update_panel = true;
			}
			else
				analyzer.startAnalysis(address, code_info, new CodeAnalyzerObserver(address));
		}
		if (update_panel)
		{
			showScrollBar(isPanelActive());
			repaint();
		}
	}

	@Override
	boolean isPanelActive() {
		return panel_state == STATE_DISPLAY_CODE;
	}

	public void updateAnalyzerState(boolean analyzer_running, int pc)
	{
		if (!analyzer_running && analyzer.isRunning())
		{
			analyzer.ackStop();
			panel_state = STATE_DISPLAY_CODE;
			if (position.getCurrentPosition() > 0)
			{
				int address = code_info.getAddressForIndex(position.getCurrentPosition());
				position.setCurrentPosition(analyzer.getDescriptionTable().getIndexForAddress(address));
			}
			code_info.setDescriptionTable(analyzer.getDescriptionTable());
			setCurrentRange(code_info.size());
			try {
				ensureVisible(code_info.getIndexForAddress(pc), false);
			}
			catch (Exception e)
			{
				// Now what?
			}
			showScrollBar(isPanelActive());
			repaint();
		}
	}

	public void go() {
		cpu.go();
		repaint();
	}

	public void halt() {
		cpu.halt();
	}

	public void stepInto() {
		is_single_step = true;
		cpu.step();
	}

	public void stepOver() {
		is_single_step = true;
		cpu.stepOver();
	}

	public void stepOut() {
		cpu.enableBreakOnReturn(true);
		cpu.go();
	}

	public boolean search(String search_text) {
		int address = -1;
		try { address = AddressRange.addressFromString(search_text); }
		catch (NumberFormatException e)	{ }
		try {
			if (address < 0)
			{	// label -> get address
				address = code_info.findLabel(search_text);
			}
			showCode(address, true);
			return true;
		}
		catch (Exception e)
		{

		}
		return false;
	}

	class CodePane extends ScrollPane
	{
		private int profile_x;
		private int profile_dx;
		private final int prof_bar_ch_width = 3;

		public CodePane()
		{
			addMouseListener(new MouseEventHandler());
			setToolTipText("Code window");
		}
		
		@Override
		public void paint(Graphics g) {
	        Dimension d = getMinimumSize();
	        g.setColor(UIManager.getColor("ScrollPane.background"));
	        g.fillRect(0,0,d.width,d.height);
	
	        g.setFont(getTextFont());
	        
	        if (isPanelActive())
	        {
				for (int i = 0; i < position.getVisibleLines(); i++)
				{
					int index = position.getCurrentPosition()+i;
					DataDescription data = code_info.getDescriptionForIndex(index);
					if (data == null)
						break;
	
					int address = data.getAddress();
					
			        g.setColor(UIManager.getColor("ScrollPane.background"));
					g.fillRect(getBorderSize(), getBorderSize()+i*getLineMetrics().getLineHeight()+1, getLineMetrics().getLineWidth(), getLineMetrics().getLineHeight()-1);

					int x = 0;
					int rc_left = getBorderSize();
					int rc_width = getLineMetrics().getLineWidth();
					
					if (profiler_active)
					{
						profile_x = getTextWidth(x);
						profile_dx = getTextWidth(prof_bar_ch_width);
						int dx_act = (profiler.getStatisticsForAddress(address).getExecutionCounter() + 9) / 10;
						if (dx_act > profile_dx)
							dx_act = profile_dx;
						g.setColor(is_running ? Color.darkGray : Color.blue);
						g.fillRect( profile_x, getBorderSize()+i*getLineMetrics().getLineHeight()+1,
									dx_act, getLineMetrics().getLineHeight()-1);
						x += prof_bar_ch_width+1;
						rc_left += profile_dx + getBorderSize();
						rc_width -= profile_dx + getBorderSize();
					}
					
					if (address == cpu.getPC())
					{
						g.setColor(is_running ? Color.gray : Color.black);
						g.drawRect(rc_left-1, getBorderSize()+i*getLineMetrics().getLineHeight(),
								   rc_width, getLineMetrics().getLineHeight());
						g.setColor(Color.blue);
					}
					else
				        g.setColor(Color.black);
					
					if (position.isSelectedAddress(address))
					{
						g.setColor(Color.blue);
						g.fillRect( rc_left, getBorderSize()+i*getLineMetrics().getLineHeight()+1,
									rc_width-1, getLineMetrics().getLineHeight()-1);
						g.setColor(Color.white);
					}
					
		            String s=("0000"+Integer.toHexString(address).toUpperCase());
		            s=s.substring(s.length()-4);
		            printString(g, s, x, i);
		            x += 5;
		            
		            try {
			            Instruction instruction = data.getInstruction(code_info.getAssembler(), code_info.getMemory());
						printString(g, instruction.getBytesShort(), x, i);
			            x += 10;
			            
			            // Label
			            assert(x == X_LABEL + getProfileColSize());
						String label = code_info.getJumpLabel(address);
						if (label != null)
						{
							if (label.length() > LABEL_MAX)
								label = label.substring(0, LABEL_MAX);
							label += ":";
							printString(g, label, x, i);
						}
			            x += LABEL_MAX + 2;
			            
			            // Opcode
			            printString(g, instruction.getOpcode(), x, i);
			            x += 5;
			            
			            // Operand
			            assert(x == X_OPERAND + getProfileColSize());
			            printString(g, instruction.getOperand(), x, i);
			            x += 24;
			            
			            // Remark
						String remark = code_info.getRemark(address);
						if (remark != null)
							printString(g, "; "+remark, x, i);
		            }
		            catch (Exception e)
		            {
				        g.setColor(Color.red);
		            	printString(g, "Internal error "+e.getClass().getName(), x, i);
		            }
				}
	        }
	        else
	        {
		        g.setColor(Color.darkGray);
	        	if (panel_state == STATE_INIT_CPU_RUNNING)
	        		printString(g, "CPU running...", 0, 0);
	        	else if (panel_state == STATE_INIT_ANALYZE_CODE)
	        		printString(g, "Analyzing...", 0, 0);
	        	else
	        		printString(g, "Internal Error", 0, 0);
	        }
		}

		private int getProfileColSize() {
			return (profiler_active ? prof_bar_ch_width+1 : 0);
		}

		public String getToolTipForLine(int line, int x) {
			if (profiler_active && x >= profile_x && x < profile_x + profile_dx)
			{
				int address = code_info.getAddressForIndex(line);
				return profiler.getStatisticsForAddress(address).getToolTip();
			}
			else
			{	// check for operand
				int i = line - position.getCurrentPosition();
				if (i >= 0)
				{
					DataDescription data = code_info.getDescriptionForIndex(line);
					if (data != null)
					{
						Instruction instruction = data.getInstruction(code_info.getAssembler(), code_info.getMemory());
						{
							Rectangle rc_operand = new Rectangle();
				            getBoundingRect(instruction.getOperand(), X_OPERAND + getProfileColSize(), i, rc_operand);
							if (x >= rc_operand.x && x < rc_operand.x + rc_operand.width)
								return instruction.getOperandDetails(cpu, code_info.getMemory());
						}
						String label = code_info.getJumpLabel(data.getAddress());
						if (label != null && label.length() > LABEL_MAX)
						{
							String short_label = label.substring(0, LABEL_MAX);
							Rectangle rc_label = new Rectangle();
				            getBoundingRect(short_label, X_LABEL + getProfileColSize(), i, rc_label);
							if (x >= rc_label.x && x < rc_label.x + rc_label.width)
								return label;
						}
					}
				}
			}
			return null;
		}

		class MouseEventHandler extends MouseAdapter
		{
			private void handleSelect(Point point) {
				requestFocusInWindow();
				int line = hitTest(point);
				int address = code_info.getAddressForIndex(line);
				position.setSelectedAddress(address);
				repaint();
			}

			/* (non-Javadoc)
			 * @see java.awt.event.MouseAdapter#mouseClicked(java.awt.event.MouseEvent)
			 */
			@Override
			public void mouseClicked(MouseEvent event) {
				if (!checkPopup(event))
					handleSelect(event.getPoint());
			}

		    public void mousePressed(MouseEvent e) { checkPopup(e); }
		    public void mouseReleased(MouseEvent e) { checkPopup(e); }
			
		    private boolean checkPopup(MouseEvent event) {
		    	if (event.isPopupTrigger()) {
		    		handleSelect(event.getPoint());
					popup.show(CodePane.this, event.getX(), event.getY());
					return true;
		    	}
		    	return false;
		    }
		}

		@Override
		public String getToolTipText(MouseEvent event) {
			Point point = event.getPoint();
			int line = hitTest(point);
			if (line >= 0)
				return getToolTipForLine(line, point.x);
			return super.getToolTipText(event);
		}
	}

	public class LineMarker extends JPanel
	{
		private int width = 24;
		
	    public LineMarker()
		{
			addMouseListener(new MouseEventHandler());
		}
	    
	    public Dimension getPreferredSize()
        {
            return getMinimumSize();
        }

        public Dimension getMinimumSize()
        {
            Dimension d=new Dimension();
            d.setSize(width, width);
            return d;
        }

		public void paint(Graphics g)
		{
	        Rectangle rec = getBounds();
	        g.setColor(UIManager.getColor("ScrollPane.background"));
	        g.fillRect(0, 0, rec.width, rec.height);
	        if (isPanelActive())
	        {
	        	int mid_x = rec.width/2;
	        	int mid_y = getBorderSize() + getLineMetrics().getLineHeight() / 2;
	        	int width = rec.width - 2*getBorderSize();
	        	int height = getLineMetrics().getLineHeight() - 2*getBorderSize();
				for (int i = 0; i < position.getVisibleLines(); i++)
				{
					int index = position.getCurrentPosition()+i;
					DataDescription data = code_info.getDescriptionForIndex(index);
					if (data == null)
						break;
					int address = data.getAddress();
					
					if (cpu.isBreakpoint(address))
					{
						g.setColor(Color.red);
						int diameter = Math.min(width, height);
						g.fillOval(mid_x-diameter/2, mid_y-diameter/2, diameter, diameter);
					}
					if (address == cpu.getPC())
					{
						Polygon p = new Polygon();
						p.addPoint(mid_x/2, mid_y - height/2);
						p.addPoint(mid_x/2, mid_y + height/2);
						p.addPoint(mid_x+width/2, mid_y);
						g.setColor(cpu.isRunning() ? Color.darkGray : Color.black);
						g.fillPolygon(p);
					}
					
					mid_y += getLineMetrics().getLineHeight();
				}
	        }
		}
		
		class MouseEventHandler extends MouseAdapter
		{
			private void handleDoubleClick(Point point) {
				int line = hitTest(point);
				int address = code_info.getAddressForIndex(line);
				cpu.breakAt(new HwNumber(address), !cpu.isBreakpoint(address));
				repaint();
			}

			/* (non-Javadoc)
			 * @see java.awt.event.MouseAdapter#mouseClicked(java.awt.event.MouseEvent)
			 */
			@Override
			public void mouseClicked(MouseEvent event) {
				if (event.getClickCount() == 2)
					handleDoubleClick(event.getPoint());
			}
			
		}
	}

	public class PopupMenuListener implements ActionListener {

		@Override
		public void actionPerformed(ActionEvent event) {
			int address = position.getSelectedAddress();
			HwNumber hw_address = new HwNumber(address);
		    if (event.getActionCommand().equals("runto"))
		    {
		    	debug_panel.onPreRun();
				cpu.stopOnceAt(hw_address);
				cpu.go();
		    }
		    else if (event.getActionCommand().equals("setbreak"))
		    {
		    	cpu.breakAt(hw_address, true);
		    }
		    else if (event.getActionCommand().equals("delbreak"))
		    {
		    	cpu.breakAt(hw_address, false);
		    }
		    else if (event.getActionCommand().equals("analyze"))
		    {
		    	analyzeAddress(address);
		    }
		    else if (event.getActionCommand().equals("topc"))
		    {
		    	showCode(cpu.getPC(), false);
		    }

		    repaint();
		}
	}

	class CodeAnalyzerObserver implements Observer, Runnable
	{
		private boolean new_state;
		private int pc;
		
		public CodeAnalyzerObserver(int pc)
		{
			this.pc = pc;
		}
		
		@Override
		public void update(Observable arg0, Object analyzer_running) {
			new_state = (Boolean)analyzer_running;
		    javax.swing.SwingUtilities.invokeLater(this);
		}
		
		@Override
		public void run() {
	    	updateAnalyzerState(new_state, pc);
		}
	}

	/* (non-Javadoc)
	 * @see emulator.shell.GUI.debugger.ScrollView#getCurrentPosition()
	 */
	@Override
	int getCurrentPosition() {
		return position.getCurrentPosition();
	}

	@Override
	void setCurrentRange(int current_range) {
		position.setCurrentRange(current_range);
	}

	/* (non-Javadoc)
	 * @see emulator.shell.GUI.debugger.ScrollView#getCurrentRange()
	 */
	@Override
	int getCurrentRange() {
		return position.getCurrentRange();
	}

	/* (non-Javadoc)
	 * @see emulator.shell.GUI.debugger.ScrollView#getVisibleLines()
	 */
	@Override
	int getVisibleLines() {
		return position.getVisibleLines();
	}

	/* (non-Javadoc)
	 * @see emulator.shell.GUI.debugger.ScrollView#setCurrentPosition(int)
	 */
	@Override
	void setCurrentPosition(int desired_position) {
		position.setCurrentPosition(desired_position);
	}

	/* (non-Javadoc)
	 * @see emulator.shell.GUI.debugger.ScrollView#setVisibleLines(int)
	 */
	@Override
	void setVisibleLines(int i) {
		position.setVisibleLines(i);
	}

	public void showCode(int address, boolean update) {
		analyzeAddress(address);
		if (update)
			position.setSelectedAddress(address);
		ensureVisible(code_info.getIndexForAddress(address), update);
	}

	public void loadSymbols(String selected_file) {
		try {
			code_info.getAssembler().loadSymbols(new FileInputStream(selected_file));
			int address = code_info.findLabel("@Start");
			if (address >= 0)
			{
				position.setSelectedAddress(address);
				ensureVisible(code_info.getIndexForAddress(address), false);
		    	if (!analyzer.isRunning())
		    		analyzer.startAnalysis(address, code_info, new CodeAnalyzerObserver(address));
			}
		} catch (FileNotFoundException e) {
			JOptionPane.showMessageDialog(null, e, "Failed to load symbols", JOptionPane.OK_OPTION);
		}
		repaint();
	}

	public String getLabel(int source) {
		return code_info.getLabel(source);
	}

	private void analyzeAddress(int address) {
		if (!analyzer.isRunning())
			analyzer.startAnalysis(address, code_info, new CodeAnalyzerObserver(address));
	}

	public void enableProfiler(boolean enable) {
		profiler_active  = enable;
		cpu.setExecutionObserver(enable ? profiler : null);
		if (isPanelActive())
			repaint();
	}
}
